package com.catchman.shiro;


import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.catchman.entity.SysUser;
import com.catchman.service.SysPermService;
import com.catchman.service.SysRoleService;
import com.catchman.service.SysUserService;
import com.catchman.vo.AuthVo;
import org.apache.shiro.authc.*;
import org.apache.shiro.authc.credential.CredentialsMatcher;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.crypto.hash.Sha256Hash;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.Set;
import java.util.stream.Collectors;

/**
 * 主要是自定义了如何查询用户信息，如何查询用户的角色和权限，如何校验密码等逻辑
 * 1、Realms：用于进行权限信息的验证，需要自己实现。Realm 本质上是一个特定的安全DAO：它封装与数据源连接的细节，得到Shiro 所需的相关的数据。
 * 2、在配置 Shiro 的时候，你必须指定至少一个Realm 来实现认证（authentication）和/或授权（authorization）。
 * @author catchman
 */
public class UserRealm extends AuthorizingRealm {

    private static final Logger logger = LoggerFactory.getLogger(UserRealm.class);

    @Autowired
    private SysUserService userService;
    @Autowired
    private SysRoleService roleService;
    @Autowired
    private SysPermService permService;

    /**
     * 数据库保存的密码是使用SHA-256算法加密的,密码匹配对象
     * Credential，证书，比如密码、指纹或者网银的Key都算是证书的一种。系统通过将用户提供的证书和系统存储的证书比较，如果一致就认为用户的身份是对的；
     * @param credentialsMatcher
     */
    @Override
    public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) {
        //设置用于匹配密码的CredentialsMatcher
        HashedCredentialsMatcher hashMatcher = new HashedCredentialsMatcher();
        //加密算法为SHA-256
        hashMatcher.setHashAlgorithmName(Sha256Hash.ALGORITHM_NAME);
        //是否存储散列后的密码为16进制，需要和生成密码时的一样，默认是base64
        hashMatcher.setStoredCredentialsHexEncoded(false);
        //1024次迭代
        hashMatcher.setHashIterations(1024);
        super.setCredentialsMatcher(hashMatcher);
    }

    /**
     * 登录后第一次访问需授权资源时才会进行授权
     * Principal，身份，就是一种用于标识用户身份的一种属性，比如邮箱、电话号码、用户名、微信号等
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        //null usernames are invalid
        if (principals == null) {
            throw new AuthorizationException("PrincipalCollection method argument cannot be null.");
        }
        SysUser user = (SysUser) getAvailablePrincipal(principals);

        Set<AuthVo> roles = user.getRoles();
        Set<AuthVo> perms = user.getPerms();
        logger.info("获取角色权限信息: roles: {}, perms: {}", roles, perms);

        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        info.setRoles(roles.stream().map(AuthVo::getVal).collect(Collectors.toSet()));
        info.setStringPermissions(perms.stream().map(AuthVo::getVal).collect(Collectors.toSet()));
        return info;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {

        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        String username = upToken.getUsername();

        if (username == null) {
            throw new AccountException("用户名不能为空");
        }
        SysUser user = userService.getOne(new QueryWrapper<SysUser>().eq("uname", username));
        if (user == null) {
            throw new UnknownAccountException("找不到用户（" + username + "）的帐号信息");
        }

        //查询用户的角色和权限存到SimpleAuthenticationInfo中，这样在其它地方
        //SecurityUtils.getSubject().getPrincipal()就能拿出用户的所有信息，包括角色和权限
        Set<AuthVo> roles = roleService.getRolesByUserId(user.getUid());
        Set<AuthVo> perms = permService.getPermsByUserId(user.getUid());
        user.getRoles().addAll(roles);
        user.getPerms().addAll(perms);

        SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPwd(), getName());
        if (user.getSalt() != null) {
            info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));
        }

        return info;
    }


}
